Skip to content

Fix leaks, round-trip failures and config bugs from the second bug hunt - #90

Merged
click0 merged 2 commits into
mainfrom
claude/refactor-data-masking-lIcWN
Nov 11, 2026
Merged

click0 merged 2 commits into
mainfrom
claude/refactor-data-masking-lIcWN

Conversation

@click0

@click0 click0 commented Nov 11, 2026 •

Copy link
Copy Markdown
Owner

Second bug hunt. It used new fuzzers (numeric entities, JSON, brackets and slashes, verb-led lines, --re-mask chains) plus audits of the configuration layer and of the command-line and I/O layer. All entries are in CHANGELOG.md under [Unreleased].

Masking leaks

  • Masks of military IDs, BR numbers, passports, tax numbers and units could equal the original, which left the number open. About 1 military ID in 100 was affected (only two digits of six change), and 1 №5дск in 10. These masks are now regenerated until they differ from the original and from the open text. Masks that already differed are unchanged.
  • Names in brackets, slashes or asterisks: (Петренко Іван Петрович) kept the surname open and lost the closing bracket.
  • Видати Петренку Івану: the infinitive was masked as a surname.
  • always_mask and custom-pattern masks were masked a second time as surnames, so unmasking returned a mask instead of the original. A custom pattern could also match a protected phrase's placeholder, and the phrase disappeared.
  • system.encoding: auto read UTF-16 as cp1251: nothing was masked, and the run still exited with 0. A BOM now decides the encoding, and round trips keep the file byte for byte.

Unmasking did not restore the original

  • JSON: every string was unmasked with its own occurrence counters, while masking numbers occurrences across the whole document. Both sides now treat all strings of a file as one text (datamasking/_jsontext.py).
  • An order-number mask could equal part of an open number (п. 1.2).
  • *старший матрос …*: the rank was masked without its first word.
  • Text dates lost the month's case, and 123 травня 2025 was masked starting from 23.
  • JSON or text is now taken from the mapping (input_format), not from the masked file's extension.

Configuration

  • exclusions match all apostrophe variants.
  • The local template no longer erases the shared lists.
  • max_name_length is null in the shared config.
  • Unknown ENV booleans are skipped with a warning; before, DATA_MASKING_ENCRYPT_OUTPUT=y produced a plaintext mapping.
  • ENV preserve_case now wins over the file.
  • router_rules filters combine with enable_*.
  • Fixed: password_generation: "false", fractional integers, --re-mask warn counts, empty sections.
  • Tracebacks replaced with errors: hash_algorithm case, backup_suffix separators, non-string logging.level, non-string section keys.
  • diagnose returns 1 for bad files.

Command line and files

  • --only/--exclude also control other document numbers.
  • JSON input with a BOM is accepted.
  • unmask applies the same size limit as mask.
  • --password works together with encrypt_output: true.
  • A password that is not UTF-8 is rejected before any file is written.
  • -o pointing to a hard link of the input is refused.
  • Empty masks in a mapping are rejected.
  • The report shows the passes actually made.
  • Tracebacks replaced with errors: damaged .enc v2 headers, --init-config into a missing folder.
  • unmask exits with 2 for wrong usage. Two assertions in tests/test_to_version.py change from 1 to 2.

Tests

New test files. The share failing on main is shown for each:

File Tests Fail on main
tests/test_fuzz_findings2.py 47 31 of the first 41
tests/test_config_findings.py 40 37
tests/test_cli_findings.py 25 22
  • Full suite: 1732 passed, 1 skipped. mypy and flake8 are clean.
  • Fuzzing after the fixes, about 6000 documents: 0 round-trip failures and 0 failures in 1000 --re-mask chains.
  • The only leaks the fuzzers still report are two-word names split across lines, a known limitation.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XT6iUWaQgahXDB9TWX9Bq7

…g hunt

Masking:
- Masks of military IDs, BR numbers, passports, tax numbers and units are
  regenerated until they differ from the original and from the open text
  (a military ID was masked as itself about once in a hundred).
- Components of open numbers (1.2, 2024-2025) count as open text, so a
  numeric mask cannot match part of them.
- Names in brackets, slashes or asterisks are recognised; the closing
  bracket is no longer swallowed by the patronymic mask.
- Infinitives (Видати, Надати) are not taken for surnames.
- Ranks after * or / are found with all their words.
- Custom-pattern and always_mask masks are held as placeholders until the
  name pass is over, so they are not masked twice; protected phrases are
  skipped by custom patterns.
- Text dates keep the month's case and do not start inside a longer number.

Unmasking:
- JSON strings are unmasked as one text, matching how masking numbers mask
  occurrences across the document; masking also treats them as one text.
- Text-date months are restored in the case found in the masked text.

Configuration:
- exclusions match all apostrophe variants; the local template no longer
  erases shared lists; max_name_length is null in the shared config.
- ENV booleans with unknown words are skipped with a warning; ENV
  preserve_case wins over the file; router_rules filters combine with
  enable_*; password_generation strings, fractional integers, re-mask warn
  counts and empty sections are handled.
- hash_algorithm case, backup_suffix separators, non-string logging.level
  and non-string section keys give errors instead of tracebacks.
- diagnose returns 1 for unreadable or malformed mapping files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XT6iUWaQgahXDB9TWX9Bq7
Generated-With: Claude Code 2.1.42
- system.encoding: auto honours a BOM (UTF-8/16/32) and keeps it byte for
  byte; text that decodes only with NUL characters is an error, not
  cp1251 (a UTF-16 file was left unmasked with exit 0).
- --only/--exclude also drive the numbers of other documents, like
  enable_orders.
- The mapping records input_format; unmask uses it instead of the masked
  file's extension (older mappings still use the extension). JSON input
  with a UTF-8 BOM is accepted.
- unmask applies the same size limit as mask.
- --password with security.encrypt_output: true is accepted; a password
  that is not valid UTF-8 is rejected before any file is written.
- -o pointing to a hard link of the input is refused.
- Mappings with an empty mask are rejected.
- The report shows the executed number of --re-mask passes.
- Damaged format-2 .enc headers and --init-config into a missing folder
  give errors instead of tracebacks.
- unmask returns 2 for wrong usage (unset --password-env variable, missing
  --map, --to-version out of range or on a non-chain mapping).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XT6iUWaQgahXDB9TWX9Bq7
Generated-With: Claude Code 2.1.42
@click0
click0 merged commit c8b8ebb into main Nov 11, 2026
16 checks passed
@click0 click0 mentioned this pull request Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants